﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;

namespace PoolKit
{
    /// <summary>
    /// Resource collection: the default implementation
    ///
    /// Note: this class is thread-safe
    /// </summary>
    /// <typeparam name="TResource"></typeparam>
    internal class ResourceCollection<TResource>: IResourceCollection<TResource>
        where TResource:class
    {
        private readonly ResourceSelectionStrategy<TResource> _resourceSelectionStrategy;
        private readonly HashSet<Pooled<TResource>> _pooledResources = new HashSet<Pooled<TResource>>();
        private readonly HashSet<Pooled<TResource>> _resourcesGivenAway = new HashSet<Pooled<TResource>>();
        private readonly object _stateChangeCriticalSection = new object();
        private readonly HashSet<Pooled<TResource>> _resourcesIntendedToBeReturned = new HashSet<Pooled<TResource>>();
        private readonly DisposableCookie _disposableCookie = new DisposableCookie(Resources.ResourceCollectionDisposed);
        
        /// <summary>
        /// 
        /// </summary>
        /// <param name="resourcesToPool"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentException"></exception>
        public ResourceCollection(IEnumerable<TResource> resourcesToPool) 
            : this(resourcesToPool, new FirstElementSelectionStrategy<TResource>())
        {			
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="resourcesToPool"></param>
        /// <param name="resourceSelectionStrategy"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentException"></exception>
        public ResourceCollection(IEnumerable<TResource> resourcesToPool, ResourceSelectionStrategy<TResource> resourceSelectionStrategy)
        {
            if (resourcesToPool == null) throw new ArgumentNullException("resourcesToPool");
            if (resourceSelectionStrategy == null) throw new ArgumentNullException("resourceSelectionStrategy");
            
            // TODO: who says it's threadsafe?
            _resourceSelectionStrategy = resourceSelectionStrategy;
            int resourcesToPoolCount = 0;

            foreach (TResource resource in resourcesToPool)
            {
                if (resource == null)
                {
                    throw new ArgumentException(Resources.NoNullsAllowedInResourceCollection, "resourcesToPool");
                }

                var pooled = new Pooled<TResource>(resource);
                _pooledResources.Add(pooled);
                resourcesToPoolCount++;
            }

            if (_pooledResources.Count < resourcesToPoolCount)
            {
                throw new ArgumentException(Resources.CollectionCanContainNoResourceTwice, "resourcesToPool");
            }
        }

        /// <summary>
        /// Tries to take a resource out of resource collection. If this succeeds, the collection still keeps track of the resource,
        /// as it can be returned back to it.
        /// </summary>
        /// <param name="resource"></param>
        /// <returns></returns>
        /// <exception cref="ObjectDisposedException"></exception>
        public bool TryTake(out TResource resource)
        {
            resource = null;

            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                if (_pooledResources.Count > 0)
                {
                    Pooled<TResource> pooledObject = _resourceSelectionStrategy.Select(_pooledResources);

                    _resourcesGivenAway.Add(pooledObject);
                    _pooledResources.Remove(pooledObject);

                    resource = pooledObject.Resource;

                    NotifyInterestedPartiesOfItemRemoval();

                    return true;
                }

                return false;
            }
        }

        /// <summary>
        ///
        /// </summary>
        /// <param name="newResource"></param>
        /// <exception cref="CannotAddResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void Add(TResource newResource)
        {
            var resourceToPool = new Pooled<TResource>(newResource);

            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                ThrowIfResourceIsDuplicateOrGivenAway(resourceToPool);

                _pooledResources.Add(resourceToPool);
            }

            NotifyInterestedPartiesOfItemArrival();
        }

        private void ThrowIfResourceIsDuplicateOrGivenAway(Pooled<TResource> resourceToPool)
        {
            if (_resourcesGivenAway.Contains(resourceToPool))
            {
                throw new CannotAddResourceException(
                    Resources.CannotAddResourceGivenAway.FormatWith(resourceToPool));
            }

            if (_pooledResources.Contains(resourceToPool))
            {
                throw new CannotAddResourceException(
                    Resources.CannotAddAnAlreadyPooledResource.FormatWith(resourceToPool));
            }
        }

        /// <summary>
        /// Register a resource making it look like it had been in collection at some point
        /// and has been taken from it.
        /// 
        /// TODO: consider removing this altogether from future releases (target of opportunity)
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="CannotAddResourceException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void RegisterResourceAsTaken(TResource resource)
        {
            var resourceToPool = new Pooled<TResource>(resource);

            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                ThrowIfResourceIsDuplicateOrGivenAway(resourceToPool);

                _resourcesGivenAway.Add(resourceToPool);
            }
        }

        /// <summary>
        /// Makes the collection forget the resource that is currently out of collection.
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="CannotForgetResourceException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void Forget(TResource resource)
        {
            var resourceToDump = new Pooled<TResource>(resource);
            
            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                if (!_resourcesGivenAway.Contains(resourceToDump))
                {
                    throw new CannotForgetResourceException(Resources.CannotForgetResource);
                }

                _resourcesGivenAway.Remove(resourceToDump);
            }
        }

        private static void DisposeResource(TResource resource)
        {
            var resourceToDispose = resource as IDisposable;
            if (resourceToDispose != null)
                resourceToDispose.Dispose();
        }

        /// <summary>
        /// Tries to evict a resource that's in the collection. If this succeeds, the collection forgets it ever had the resource.
        /// </summary>
        /// <param name="resourceToEvict"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public bool TryEvict(TResource resourceToEvict)
        {
            var resourceToDump = new Pooled<TResource>(resourceToEvict);
            bool removed = false;

            try
            {
                lock (_stateChangeCriticalSection)
                {
                    _disposableCookie.ThrowIfDisposed();

                    if (_resourcesGivenAway.Contains(resourceToDump))
                    {
                        return false;
                    }

                    if (!_pooledResources.Contains(resourceToDump))
                    {
                        return false;
                    }

                    _pooledResources.Remove(resourceToDump);

                    DisposeResource(resourceToDump.Resource);

                    removed = true;
                    return true;
                }
            }
            finally
            {
                if (removed)
                {
                    NotifyInterestedPartiesOfItemRemoval();
                }
            }
        }

        /// <summary>
        /// Note: it's assumed that after calling this one, the pool will actually call AcceptBack or AcceptBackBroken
        /// (a somewhat weird internal contract to ensure consistency in the face of asynchronous interaction).
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void ReserveRightToReturnResource(TResource resource)
        {
            var resourceToPool = new Pooled<TResource>(resource);

            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                if (_resourcesIntendedToBeReturned.Contains(resourceToPool))
                {
                    throw new InvalidOperationException(Resources.DuplicateReturnReservation.FormatWith(resourceToPool.ToString()));
                }

                if (HasNotBeenGivenAway(resourceToPool))
                {
                    throw new InvalidOperationException(Resources.CannotReserveRightToReturnNonOutOfPoolResource.FormatWith(resourceToPool.ToString()));
                }

                _resourcesIntendedToBeReturned.Add(resourceToPool);
            }
        }

        /// <summary>
        /// Count of resources that currently are in collection (that is, without those which have been taken away)
        /// </summary>
        public int Count
        {
            get
            {
                return _pooledResources.Count;
            }
        }

        public event EventHandler ItemCountUp;
        public event EventHandler ItemCountDown;

        private void NotifyInterestedPartiesOfItemArrival()
        {
            RaiseEventInThreadSafeWay(ItemCountUp);
        }

        private void RaiseEventInThreadSafeWay(EventHandler eventHandler)
        {
            EventHandler handler = Interlocked.CompareExchange(ref eventHandler, null, null);

            if (handler != null)
            {
                handler.Invoke(this, EventArgs.Empty);
            }
        }

        private void NotifyInterestedPartiesOfItemRemoval()
        {
            RaiseEventInThreadSafeWay(ItemCountDown);
        }

        private bool HasNotBeenGivenAway(Pooled<TResource> resourceToPool)
        {
            return !_resourcesGivenAway.Contains(resourceToPool);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="resource"></param>
        /// <exception cref="CannotAcceptResourceException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void AcceptBack(TResource resource)
        {
            var resourceToPool = new Pooled<TResource>(resource);

            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                // unreserving intention to return
                _resourcesIntendedToBeReturned.Remove(resourceToPool);

                ThrowIfResourceHasNotBeenGivenAway(resourceToPool);

                _pooledResources.Add(resourceToPool);

                _resourcesGivenAway.Remove(resourceToPool);
            }

            NotifyInterestedPartiesOfItemArrival();
        }

        /// <exception cref="CannotAcceptResourceException"></exception>
        private void ThrowIfResourceHasNotBeenGivenAway(Pooled<TResource> resourceToPool)
        {
            if (HasNotBeenGivenAway(resourceToPool))
            {
                throw new CannotAcceptResourceException(Resources.CannotAcceptResourceThatIsNotAwayInternalFailure);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="resourceBeingSubstituted"></param>
        /// <param name="substitution"></param>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="CannotAcceptResourceException"></exception>
        /// <exception cref="ReplacingResourceProducesDuplicateException"></exception>
        /// <exception cref="ObjectDisposedException"></exception>
        public void AcceptSubstitution(TResource resourceBeingSubstituted, TResource substitution)
        {
            if (resourceBeingSubstituted == null) throw new ArgumentNullException("resourceBeingSubstituted");
            if (substitution == null) throw new ArgumentNullException("substitution");

            var oldResourcePooled = new Pooled<TResource>(resourceBeingSubstituted);
            var newResourcePooled = new Pooled<TResource>(substitution);

            lock (_stateChangeCriticalSection)
            {
                _disposableCookie.ThrowIfDisposed();

                ThrowIfResourceHasNotBeenGivenAway(oldResourcePooled);

                // unreserving intention to return
                _resourcesIntendedToBeReturned.Remove(oldResourcePooled);
                
                if (_pooledResources.Contains(newResourcePooled))
                {
                    throw new ReplacingResourceProducesDuplicateException(Resources.SubstitutionsProducingDuplicatesIsProhibited);
                }

                _resourcesGivenAway.Remove(oldResourcePooled);

                _pooledResources.Add(newResourcePooled);
            }

            NotifyInterestedPartiesOfItemArrival();
        }

        /// <summary>
        /// Disposes the resource collection, disposing every pooled resource if the resource implementes IDisposable.
        /// Lent resources are not disposed.
        /// </summary>
        public void Dispose()
        {
            lock (_stateChangeCriticalSection)
            {
                if (_disposableCookie.IsDisposed)
                {
                    return;
                }

                if (_pooledResources.Count != 0)
                {
                    foreach (var pooledResource in _pooledResources)
                        DisposeResource(pooledResource.Resource);

                    _pooledResources.Clear();

                    // NotifyInterestedPartiesOfItemRemoval is not called as oppose to TryEvict method.
                }

                _disposableCookie.MarkAsDisposed();
            }
        }
    }
}